package com.github.wxiaoqi.security.crm.core.blockchain;

import com.github.wxiaoqi.security.crm.core.blockchain.block.Block;
import com.github.wxiaoqi.security.crm.core.blockchain.block.DposBlock;
import com.github.wxiaoqi.security.crm.core.blockchain.transaction.*;
import com.github.wxiaoqi.security.crm.core.blockchain.util.Base58Check;
import com.github.wxiaoqi.security.crm.core.blockchain.util.KeyUtil;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.time.Instant;

/**
 * @author: bolei
 * @date：2018年9月9日 下午2:10:45
 * @description：类说明
 */

/**
 * 火钻的产生：火钻分为两类：1.计算火钻（由区块链产生） 2.充值火钻（充值产生）
 * <p>
 * <p>
 * 每个地址在创建时，都会奖励一定量的计算火钻（因为商户链是根据交易数量打包，而不是根据时间打包，
 * 所以会存在一个初期没有交易的死循环，所以前期奖励一定量的计算火钻是必须的），这样顾客在使用计算火钻时便产生了
 * 初始交易，这样商户链就可以启动起来了。然后在打包区块时产生一定量的计算火钻，按照再按照原力值按比例进行分配。
 * <p>
 * <p>
 * <p>
 * 顾客注册 ：1.生成地址 2.判断是否是前期种子用户发放计算火钻奖励。3.顾客按照原力获取方式获取原力。
 * <p>
 * 使用：顾客使用计算火钻进行结算，火钻由顾客地址转移到商户地址，产生一笔交易。区块打包，产生奖励，再按照原力值按比例进行分配
 * <p>
 * <p>
 * ？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？
 * 此处即使有奖励也有一个漏洞。因为计算火钻是由顾客结付给商家的单向交易，如果区块打包出来的计算火钻数量没有达到交易的数量
 * 就会产生一个顾客手中没有火钻，商家拥有绝大部分的火钻。最终整个商户链又会出现没有交易的死循环。
 * <p>
 * 比如说顾客A有两个火钻，顾客B也有两个火钻 ，他俩进行消费，转账给商户，产生了两笔交易。区块打包这两笔交易产生了0.5个火钻。
 * 分配给A,B每人0.25个火钻，这0.25个火钻不足以支付任何东西，交易又一次被归零。。。。
 * <p>
 * 看来这个是个按照交易量来打包的死结。即使开放了顾客和顾客之间的转账、商户和顾客之间的互转，但是火钻的消费尽头只能是商家。
 * 火钻越集中，产生的交易数量就越少
 * 是否可以改成按照时间打包，比如说打包两个小时内的交易。这样，也不需要每个账户进行奖励，
 * 也不会出现区块链无法增长的尴尬。
 * <p>
 * <p>
 * ？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？？
 */
public class PlatformContract {

    //根据平台id获取平台coin配置，根据配置文件进行区块奖励和生成区块及进行相应的入账操作

    //给平台币入账 该入账包写表 platform_coin_accout platform_coin_accout_journal
    //同时写表personal_account和表personal_account_journal
    //platform_coin_accout


    /**
     * 生成区块每次产生交易调用该方法
     *
     * @param platformId
     */
    public void mineBlock(Long platformId) {
        if (isCreateBlock(platformId)) {
            String preBlockHash = getTopBlockHash(platformId);
            if (preBlockHash == null) {
                String rewardAddress = rewardAddress(platformId);
                //作为区块奖励给奖励地址发放奖励
                DposTransaction transaction = new DposTransaction();
                DposTXInput txInput = new DposTXInput();
                DposTXOutput txOutput = DposTXOutput.newDposTXOutput(allReward(),rewardAddress);
                transaction.setInputs(new DposTXInput[]{txInput});
                transaction.setOutputs(new DposTXOutput[]{txOutput});
                transaction.setCreateTime(Instant.now().getEpochSecond());
                transaction.setTxType(TransactionType.Reward.getCode());
                transaction.setTxId(transaction.hash());
                saveTransactionToPool(transaction,null);
                DposBlock block = DposBlock.newGenesisBlock(transaction, platformId);
                saveBlockToChain(block, platformId);
                preBlockHash = getTopBlockHash(platformId);
            }
            DposTransaction[] transactions = getUnPackedTransaction(platformId);
            DposBlock block = DposBlock.newBlock(preBlockHash, transactions, platformId);
            setTransactionBlockHash(block);
            saveBlockToChain(block, platformId);
        }
    }

    /**
     * 设置交易所属区块hash
     * 更新交易所属区块hash
     * @param block
     */
    private void setTransactionBlockHash(DposBlock block) {

    }


    /**
     * 开始交易
     * <p>
     * 流程为：
     * 1.发起交易请求
     * 2.先判断转出方地址是否有足够的余额支付
     * 3.判断转入方地址是否合法存在
     * 4.对交易进行签名
     * 5.将交易放入交易池或者写入数据库中，在此同时判断交易签名正确与否
     * 6.判断是否应该创建区块 然后 打包
     *
     * @param from       转出方地址
     * @param to         转入方地址
     * @param amount     数量
     * @param platformId 商户id
     * @param txType     交易类型
     * @return 交易是否成功
     */
    public boolean startTransaction(String from, String to, double amount, Long platformId, String txType, BCECPrivateKey privateKey, BCECPublicKey publicKey) {
        if (isEnoughToPay(from, amount, platformId)) {
            if (isAddresslegal(to, platformId)) {
                DposTransaction transaction = initTransaction(from, to, amount, platformId, txType,publicKey);
                signTransaction(transaction, privateKey);
                if (saveTransactionToPool(transaction, publicKey)) {
                    mineBlock(platformId);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 保存交易到交易池，在保存时验证交易
     *
     * @param transaction
     * @return 保存成功 返回true
     */
    private boolean saveTransactionToPool(DposTransaction transaction, BCECPublicKey publicKey) {
        if (verifyTransactionSignature(transaction, publicKey)) {
            //保存

            return true;
        } else {
            return false;
        }
    }

    /**
     * 创建交易
     *
     * @param from       转出方地址
     * @param to         转入方地址
     * @param amount     数量
     * @param platformId 商户id
     * @param txType     交易类型
     * @return
     */
    private DposTransaction initTransaction(String from, String to, double amount, Long platformId, String txType, BCECPublicKey publicKey) {
        DposTXInput txInput = new DposTXInput(from,amount);
        DposTXOutput txOutput = new DposTXOutput(amount,to);
        DposTransaction dposTransaction = new DposTransaction(null,new DposTXInput[]{txInput},new DposTXOutput[]{txOutput},
                Instant.now().getEpochSecond(),"", KeyUtil.publicKeyToString(publicKey),
                "",txType);
        dposTransaction.setTxId(dposTransaction.hash());
        return null;
    }

    /**
     * 对交易进行签名
     * @param transaction
     * @param privateKey
     */
    public void signTransaction(DposTransaction transaction, String privateKey) {
        try {
            signTransaction(transaction,KeyUtil.stringToPrivateKey(privateKey));
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
    }

    /**
     * 对交易进行签名
     * @param transaction
     * @param privateKey
     */
    public void signTransaction(DposTransaction transaction, BCECPrivateKey privateKey) {
        try {
          String signature= Base58Check.bytesToBase58(KeyUtil.sign(privateKey,transaction.getTxId())) ;
          transaction.setSignature(signature);
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (SignatureException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
    /**
     * 验证签名
     * @param transaction 交易
     * @param publicKey 公钥
     * @return
     */
    public boolean verifyTransactionSignature(DposTransaction transaction, String publicKey){
        try {
            return verifyTransactionSignature(transaction,KeyUtil.stringToPublicKey(publicKey));
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return false;
    }
    /**
     * 验证签名
     * @param transaction 交易
     * @param publicKey 公钥
     * @return
     */
    public boolean verifyTransactionSignature(DposTransaction transaction, BCECPublicKey publicKey) {
        String signature = transaction.getSignature();
        //区块奖励 不验证
        if (transaction.getTxType().equals(TransactionType.Reward.getCode())){
            return true;
        }
        try {
            return KeyUtil.verify(publicKey,Base58Check.base58ToBytes(signature),transaction.getTxId());
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (SignatureException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 获取是否到达打包数量，如果达到了打包数量 进行打包
     * 对platform_coin_transaction_from表中serial_number字段参数210取余判断是否应该生成区块
     *
     * @return
     */
    private boolean isCreateBlock(Long platformId) {

        return true;
    }

    /**
     * 获取当前区块头的hash，
     *从platform_blockchain中获取最新的一个区块
     * @return 如果没有区块，则返回null
     */
    private String getTopBlockHash(Long platformId) {
        return "";
    }

    /**
     * 获取未打包交易
     *
     * @param platformId
     * @return
     */
    private DposTransaction[] getUnPackedTransaction(Long platformId) {
        return null;
    }

    /**
     * 保存区块
     *
     * @param block
     */
    private void saveBlockToChain(DposBlock block, Long platformId) {

    }

    /**
     * 判断是否是前期种子用户，前期注册的用户会分享商家火钻池里面一定比例的火钻作为奖励，
     * 需要定义好前期多少人享受奖励和多大比例的火钻作为奖励
     * 用户生成时 是否可以添加一个序号（比如 这是第几位注册的）
     * 在platform_coin_customer中添加第几位？
     *
     * @param address
     * @return
     */
    private boolean isLuckAddress(String address, Long platformId) {
        return true;
    }

    /**
     * 设置初始奖励，给前期种子用户发放奖励。
     * 用户账户创建时调用，用于分配账户初始余额，避免无法产生交易
     *
     * @param address 种子用户地址
     */
    public void dispatchLuckReward(String address, Long platformId,BCECPublicKey rewardPubkey,BCECPrivateKey rewardPriKey) {
        if (isLuckAddress(address, platformId)&&isEnoughToPay(rewardAddress(platformId),rewardNum(),platformId)) {

            DposTXInput input = new DposTXInput(rewardAddress(platformId),rewardNum());
            DposTXOutput output = new DposTXOutput(rewardNum(),address);
            DposTransaction tx = new DposTransaction(null,new DposTXInput[]{input},new DposTXOutput[]{output},
                    Instant.now().getEpochSecond(),"", KeyUtil.publicKeyToString(rewardPubkey),
                    "", TransactionType.Transfer.getCode());
            tx.setTxId(tx.hash());
            signTransaction(tx,rewardPriKey);
            saveTransactionToPool(tx,rewardPubkey);
            //............
            // 保存一条交易
        }
    }

    /**
     * 判断地址是否有足够的余额去支付
     * 从platform_coin_customer表中根据platformId 和address 来获取账户余额，
     * @param address
     * @return 足够 返回true
     */
    private boolean isEnoughToPay(String address, double amount, Long platformId) {
        return true;
    }

    /**
     * 判断地址是否存在
     * 从platform_coin_customer表中根据platformId查看是否存在地址address
     * @param address
     * @return 存在 返回true
     */
    private boolean isAddresslegal(String address, Long platformId) {
        return true;
    }

    /**
     * 返回奖励地址，这个奖励地址的账户内含有初始配置时所有的额度，
     * 创建这个奖励地址是为了与普通的交易统一起来，把奖励看为一个普通交易
     * 当新建用户时，就要从奖励地址发放奖励给用户
     * 这个应在平台建立之后，自动生成。
     * 放在platform_coin_config？
     * @param platformId
     * @return
     */
    private String rewardAddress(Long platformId){
        return "";
    }

    /**
     * 奖励给每个种子用户的数额。
     * 当平台创建时配置，奖励给每个种子用户的数额，比如说 ： 每个种子用户奖励一个火钻
     *（建议：platform_coin_config）
     * @return
     */
    private double rewardNum(){
        return 0;
    }

    /**
     * 要发放的所有的奖励的数额，应在平台创建时配置
     * （建议：platform_coin_config）
     * @return
     */
    private double allReward(){
        return 0;
    }
}

